πŸ”— PR - https://github.com/grpc/grpc-go/pull/8375

1. PR의 λͺ©μ 

기쑴에 μž‘μ„±λ˜μ§€ μ•Šμ•˜λ˜ ExitIdle κ΄€λ ¨ λ©”μ„œλ“œμ˜ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜μ—¬ ν…ŒμŠ€νŠΈ 컀버리λ₯Ό μΆ”κ°€ν•˜λŠ” PR μž…λ‹ˆλ‹€. μ—¬κΈ°μ„œ ExitIdle 이 무엇을 μ˜λ―Έν•˜λŠ”μ§€ μ•„λž˜μ—μ„œ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€. 이번 PR μ—μ„œλŠ” BalancerGroup 의 ExitIdle 이 λ‹€μ–‘ν•œ μ‹œλ‚˜λ¦¬μ˜€μ—μ„œ μ˜¬λ°”λ₯΄κ²Œ μ²˜λ¦¬λ˜λŠ”μ§€ κ²€μ¦ν•˜κΈ° μœ„ν•œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

1-1. ExitIdle μ΄λž€?

ExitIdle은 gRPC ν΄λΌμ΄μ–ΈνŠΈκ°€ 유휴(idle) μƒνƒœμ—μ„œ λ²—μ–΄λ‚˜(exit) λ‹€μ‹œ 연결을 ν™œμ„±ν™”ν•˜λŠ” λ©”μ»€λ‹ˆμ¦˜μ„ μ˜λ―Έν•©λ‹ˆλ‹€. gRPC ν΄λΌμ΄μ–ΈνŠΈλŠ” μ„œλ²„μ™€μ˜ 연결을 μœ μ§€ν•˜μ§€λ§Œ, 일정 μ‹œκ°„ λ™μ•ˆ μš”μ²­μ΄ μ—†μœΌλ©΄ λ¦¬μ†ŒμŠ€λ₯Ό μ ˆμ•½ν•˜κΈ° μœ„ν•΄ 연결이 유휴 μƒνƒœλ‘œ μ „ν™˜λ©λ‹ˆλ‹€. 유휴 μƒνƒœμ—μ„œλŠ” μ„œλ²„μ™€μ˜ 연결이 λŠκΈ°κ±°λ‚˜, μš”μ²­μ΄ 왔을 λ•Œ μ¦‰μ‹œ μ²˜λ¦¬ν•  수 μ—†λŠ” μƒνƒœκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€. ExitIdle은 μƒˆλ‘œμš΄ μš”μ²­μ΄ 듀어왔을 λ•Œ, ν΄λΌμ΄μ–ΈνŠΈκ°€ 유휴 μƒνƒœμ—μ„œ λ²—μ–΄λ‚˜ μ„œλ²„μ™€μ˜ 연결을 μž¬κ°œν•˜κ³ , 정상적인 μš”μ²­ 처리 μƒνƒœλ‘œ λŒμ•„κ°€λ„λ‘ νŠΈλ¦¬κ±°ν•©λ‹ˆλ‹€.

subConn 은 gRPC client 와 ν•˜λ‚˜μ˜ νŠΉμ • μ„œλ²„ κ°„μ˜ 단일 연결을 μ˜λ―Έν•˜λŠ” κ°œλ…μœΌλ‘œ, IDLE μƒνƒœμ˜ SubConn 은 subConn.Connectκ°€ 호좜될 λ•ŒκΉŒμ§€ μžλ™μœΌλ‘œ μž¬μ—°κ²°ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. gRPC 의 default idleTimeout 은 30λΆ„μœΌλ‘œ 채널에 μ§„ν–‰ 쀑인 RPCκ°€ μ—†κ³  μƒˆλ‘œμš΄ RPCκ°€ μ‹œμž‘λ˜μ§€ μ•ŠμœΌλ©΄ 채널은 idle λͺ¨λ“œλ‘œ μ „ν™˜λ©λ‹ˆλ‹€. μ΄λ•Œ name resolver와 load balancerκ°€ μ’…λ£Œλ©λ‹ˆλ‹€. λ°˜λŒ€λ‘œ ExitIdle 이 트리거 되면 name resolver 와 load balancer μ—­μ‹œ μž¬μ‹œμž‘ λ©λ‹ˆλ‹€.

func defaultDialOptions() dialOptions {
	return dialOptions{
		copts: transport.ConnectOptions{
			ReadBufferSize:  defaultReadBufSize,
			WriteBufferSize: defaultWriteBufSize,
			UserAgent:       grpcUA,
			BufferPool:      mem.DefaultBufferPool(),
		},
		bs:                       internalbackoff.DefaultExponential,
		idleTimeout:              30 * time.Minute,
		defaultScheme:            "dns",
		maxCallAttempts:          defaultMaxCallAttempts,
		useProxy:                 true,
		enableLocalDNSResolution: false,
	}
}
gRPC 의 channel μ΄λž€ `채널(channel)` 은 ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ— RPC(remote procedure call) λ₯Ό 보낼 수 μžˆλŠ” 톡신 ν†΅λ‘œ 역할을 ν•˜λŠ” 논리적 연결을 μ˜λ―Έν•©λ‹ˆλ‹€. μ—¬κΈ°μ„œ 채널은 λ‹¨μˆœνžˆ ν•˜λ‚˜μ˜ tcp 연결을 μ˜λ―Έν•˜λŠ” 것이 μ•„λ‹Œ, μ—¬λŸ¬ κΈ°λŠ₯을 μΆ”μƒν™”ν•˜κ³  κ΄€λ¦¬ν•˜λŠ” μƒμœ„ κ°œλ…μœΌλ‘œ μ•„λž˜μ˜ 역할을 μˆ˜ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ—°κ²° 관리 : 채널은 ν•œ 개 μ΄μƒμ˜ 물리적인 TCP μ—°κ²°(subConn) 을 관리
  • λΆ€ν•˜ λΆ„μ‚° : μ—¬λŸ¬ μ„œλ²„ μΈμŠ€ν„΄μŠ€μ— λΆ€ν•˜ 뢄산을 처리
  • μ—°κ²° μž¬μ‹œλ„: 연결이 λŠμ–΄μ‘Œμ„ λ•Œ μžλ™ μž¬μ—°κ²° μ‹œλ„.
type ExitIdler interface {
    // ExitIdle은 LB μ •μ±…μ—κ²Œ λ°±μ—”λ“œμ™€ μž¬μ—°κ²°ν•˜κ±°λ‚˜
    // IDLE μƒνƒœμ—μ„œ λ²—μ–΄λ‚˜λ„λ‘ μ§€μ‹œν•©λ‹ˆλ‹€
    ExitIdle()
}

μ •λ¦¬ν•˜μžλ©΄ ExitIdle 은 유휴 μƒνƒœμΈ ν΄λΌμ΄μ–ΈνŠΈκ°€ ν™œμ„±ν™”λ˜μ–΄ μž¬μš”μ²­μ„ λ³΄λ‚΄λŠ” 트리거 역할을 μ œκ³΅ν•©λ‹ˆλ‹€.

1-2. BalancerGroup μ΄λž€?

balancergroup은 gRPC ν΄λΌμ΄μ–ΈνŠΈ μΈ‘μ—μ„œ μ—¬λŸ¬ λ°±μ—”λ“œ 연결을 κ΄€λ¦¬ν•˜κ³  μš”μ²­μ„ μ μ ˆν•œ λ°±μ—”λ“œλ‘œ λΆ„μ‚°ν•˜κΈ° μœ„ν•œ λΆ€ν•˜ λΆ„μ‚° ꡬ성 μš”μ†Œμž…λ‹ˆλ‹€. ν•˜λ‚˜μ˜ gRPC μ„œλΉ„μŠ€λŠ” μ—¬λŸ¬ μ„œλ²„(λ°±μ—”λ“œ)둜 ꡬ성될 수 있으며, balancergroup 은 μ΄λŸ¬ν•œ μ„œλ²„λ₯Ό κ·Έλ£Ήν™”ν•˜μ—¬ 효율적인 λΆ€ν•˜ 뢄산을 κ°€λŠ₯μΌ€ ν•©λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈμ™€ 톡신할 μ—¬λŸ¬ μ„œλ²„ μ€‘μ—μ„œ ν˜„μž¬ μš”μ²­μ„ 보낼 졜적의 μ„œλ²„λ₯Ό μ„ νƒν•˜λŠ” λ‘œμ§μ„ κ΅¬ν˜„ν•˜λ©°, μ΄λŠ” round robin,least-connection 같은 λ‹€μ–‘ν•œ λΆ€ν•˜ λΆ„μ‚° μ „λž΅μ„ 선택할 수 μžˆμŠ΅λ‹ˆλ‹€. balancergroup 은 ν΄λΌμ΄μ–ΈνŠΈ-μ„œλ²„ κ°„ μ—¬λŸ¬ 개의 물리적 연결을 논리적인 ν•˜λ‚˜μ˜ λ‹¨μœ„λ‘œ κ΄€λ¦¬ν•˜μ—¬, λ‘œλ“œ λ°ΈλŸ°μ‹± 정책이 λ³΅μž‘ν•œ μ—°κ²° μƒνƒœλ₯Ό κ°œλ°œμžκ°€ 직접 닀루지 μ•Šκ³ λ„ μΆ”μƒν™”λœ λ°±μ—”λ“œ 그룹에 λŒ€ν•œ 결정을 내릴 수 μžˆλŠ” 역할을 ν•©λ‹ˆλ‹€.

ExitIdle κ³Ό BalancerGroup λŠ” ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ λ‘œλ“œ λ°ΈλŸ°μ‹±μ—μ„œ ν•¨κ»˜ λ™μž‘ν•©λ‹ˆλ‹€. 유휴 μƒνƒœμ˜ ν΄λΌμ΄μ–ΈνŠΈκ°€ ν™œμ„±ν™”λ˜μ–΄ μš”μ²­μ„ 보내기 μœ„ν•œ 트리거 역할을 ExitIdle 이 μ œκ³΅ν•˜κ³ , BalancerGroup 은 ν™œμ„±ν™”λœ ν΄λΌμ΄μ–ΈνŠΈκ°€ μš”μ²­μ„ 보낼 λ•Œ 졜적의 μ„œλ²„λ₯Ό 선택할 μžˆλ„λ‘ λΆ€ν•˜ λΆ„μ‚° μž‘μ—…μ„ μ„€μ • 역할을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.

2. PR - ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ μž‘μ„±

BalancerGroup κ΄€λ ¨ ν…ŒμŠ€νŠΈ μž‘μ„±:

2-1. TestBalancerGroup_UpdateClientConnState_AfterClose

BalancerGroup이 λ‹«νžŒ(Close) ν›„ UpdateClientConnState λ©”μ„œλ“œκ°€ ν˜ΈμΆœλμ„ λ•Œμ˜ λ™μž‘μ„ κ²€μ¦ν•˜λŠ” ν…ŒμŠ€νŠΈλ‘œ, BalancerGroup close ν›„μ—λŠ” 더 이상 ν•˜μœ„ balancer μ—κ²Œ μƒνƒœ μ—…λ°μ΄νŠΈ μ „νŒŒκ°€ λ˜μ§€ μ•Šλ„λ‘ κ²€μ¦ν•©λ‹ˆλ‹€.

func (s) TestBalancerGroup_UpdateClientConnState_AfterClose(t *testing.T) {
	balancerName := t.Name()
	clientConnStateCh := make(chan struct{}, 1)

  // stub balancer 등둝
	stub.Register(balancerName, stub.BalancerFuncs{
	  // UpdateClientConnState 호좜될 λ•Œ λ§ˆλ‹€ 채널에 μ‹ ν˜Έλ₯Ό 보내도둝 함
		UpdateClientConnState: func(_ *stub.BalancerData, _ balancer.ClientConnState) error {
			clientConnStateCh <- struct{}{}
			return nil
		},
	})

	bg := New(Options{
		CC:              testutils.NewBalancerClientConn(t), // ν…ŒμŠ€νŠΈμš© ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° 생성
		BuildOpts:       balancer.BuildOptions{},
		StateAggregator: nil,
		Logger:          nil,
	})

	bg.Add(testBalancerIDs[0], balancer.Get(balancerName))
	bg.Close() // BalancerGroup 을 μ’…λ£Œ μƒνƒœλ‘œ λ³€κ²½ -> bg 에 μ†ν•˜λŠ” balancer λŠ” 유휴 μƒνƒœκ°€ λ˜μ–΄μ•Ό 함

	if err := bg.UpdateClientConnState(testBalancerIDs[0], balancer.ClientConnState{}); err != nil {
		t.Fatalf("Expected nil error, got %v", err)
	}

	select {
	case <-clientConnStateCh:
		t.Fatalf("UpdateClientConnState was called after BalancerGroup was closed")
	case <-time.After(defaultTestShortTimeout):
	}
}

UpdateClientConnState λ₯Ό ν˜ΈμΆœν•˜λ©΄ clientConnStateCh 채널에 μ‹ ν˜Έκ°€ μ˜€λ„λ‘ stub 을 λ“±λ‘ν–ˆκ³ , select 문을 μ‚¬μš©ν•˜μ—¬ ν•΄λ‹Ή 채널에 μ‹ ν˜Έκ°€ μ˜€λŠ”μ§€ ν˜Ήμ€ defaultTestShortTimeout 이 μ§€λ‚˜λ©΄ μ •μƒμ’…λ£Œ λ˜λ„λ‘ 두가지 μΌ€μ΄μŠ€λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€. μ’…λ£Œλœ BalancerGroup μ—μ„œ UpdateClientConnState λ₯Ό ν˜ΈμΆœν•  수 μ—†λŠ” 것이 정상 λ™μž‘μ΄κΈ° λ•Œλ¬Έμ— clientConnStateCh κ°€ μ‹ ν˜Έλ₯Ό λ°›μœΌλ©΄ ν…ŒμŠ€νŠΈλŠ” μ‹€νŒ¨ν•©λ‹ˆλ‹€. μ‹€μ œλ‘œ UpdateClientConnState κ΅¬ν˜„μ„ 보면 balancer group 이 λ‹«νžŒ ν›„μ—” nil 을 λ¦¬ν„΄ν•˜μ—¬ graceful ν•˜κ²Œ μ²˜λ¦¬λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

// UpdateClientConnState handles ClientState (including balancer config and
// addresses) from resolver. It finds the balancer and forwards the update.
func (bg *BalancerGroup) UpdateClientConnState(id string, s balancer.ClientConnState) error {
	bg.outgoingMu.Lock()
	defer bg.outgoingMu.Unlock()
	if bg.outgoingClosed {
		return nil
	}
	if config, ok := bg.idToBalancerConfig[id]; ok {
		return config.updateClientConnState(s)
	}
	return nil
}

2-2. TestBalancerGroup_ResolverError_AfterClose

BalancerGroup이 λ‹«νžŒ ν›„ ResolverError λ©”μ„œλ“œκ°€ 호좜 μ‹œ λ™μž‘μ„ κ²€μ¦ν•˜λŠ” ν…ŒμŠ€νŠΈλ‘œ BalancerGroup close ν›„μ—λŠ” 더 이상 ν•˜μœ„ balancer μ—κ²Œ resolver μ—λŸ¬κ°€ μ „νŒŒλ˜μ§€ μ•Šλ„λ‘ κ²€μ¦ν•©λ‹ˆλ‹€. ResolverErrorλŠ” gRPC의 name resolution κ³Όμ •μ—μ„œ λ°œμƒν•˜λŠ” μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•˜λŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€. DNS 쑰회 μ‹€νŒ¨, Service Discovery μž₯μ• , λ„€νŠΈμ›Œν¬ μ—°κ²° 문제 λ“±μ˜ μƒν™©μ—μ„œ name resolverκ°€ μ„œλΉ„μŠ€ 이름을 μ‹€μ œ μ„œλ²„ μ£Όμ†Œλ‘œ λ³€ν™˜ν•˜μ§€ λͺ»ν•  λ•Œ ν˜ΈμΆœλ©λ‹ˆλ‹€. ResolverErrorλ₯Ό ν˜ΈμΆœν•˜λ©΄ resolveErrorCh 채널에 μ‹ ν˜Έκ°€ μ˜€λ„λ‘ stub을 λ“±λ‘ν–ˆκ³  select 문을 μ‚¬μš©ν•˜μ—¬ ν•΄λ‹Ή 채널에 μ‹ ν˜Έκ°€ μ˜€λŠ”μ§€ ν˜Ήμ€ defaultTestShortTimeout이 μ§€λ‚˜λ©΄ μ •μƒμ’…λ£Œ λ˜λ„λ‘ 두가지 μΌ€μ΄μŠ€λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€. μ’…λ£Œλœ BalancerGroup μ—μ„œ ResolverErrorκ°€ ν•˜μœ„ balancer에 μ „νŒŒλ˜μ§€ μ•ŠλŠ” 것이 정상 λ™μž‘μ΄κΈ° λ•Œλ¬Έμ— resolveErrorChκ°€ μ‹ ν˜Έλ₯Ό λ°›μœΌλ©΄ ν…ŒμŠ€νŠΈλŠ” μ‹€νŒ¨ν•©λ‹ˆλ‹€.

func (s) TestBalancerGroup_ResolverError_AfterClose(t *testing.T) {
	balancerName := t.Name()
	resolveErrorCh := make(chan struct{}, 1)

	stub.Register(balancerName, stub.BalancerFuncs{
		ResolverError: func(_ *stub.BalancerData, _ error) {
			resolveErrorCh <- struct{}{}
		},
	})

	bg := New(Options{
		CC:              testutils.NewBalancerClientConn(t),
		BuildOpts:       balancer.BuildOptions{},
		StateAggregator: nil,
		Logger:          nil,
	})

	bg.Add(testBalancerIDs[0], balancer.Get(balancerName))
	bg.Close()

	bg.ResolverError(errors.New("test error"))

	select {
	case <-resolveErrorCh:
		t.Fatalf("ResolverError was called on sub-balancer after BalancerGroup was closed")
	case <-time.After(defaultTestShortTimeout):
	}
}

ResolverError κ΅¬ν˜„μ—μ„  balancer group 이 λ‹«νžŒ ν›„μ—” early return 으둜 graceful ν•œ μ²˜λ¦¬ν•˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

// ResolverError handles name resolution errors from the name resolver.
func (bg *BalancerGroup) ResolverError(err error) {
    bg.outgoingMu.Lock()
    defer bg.outgoingMu.Unlock()
    if bg.outgoingClosed {
        return
    }
    for _, config := range bg.idToBalancerConfig {
        config.resolverError(err)
    }
}

2-3. TestBalancerGroup_ExitIdle_AfterClose

BalancerGroup이 λ‹«νžŒ ν›„ ExitIdle λ©”μ„œλ“œκ°€ ν˜ΈμΆœλμ„ λ•Œμ˜ λ™μž‘μ„ κ²€μ¦ν•˜λŠ” ν…ŒμŠ€νŠΈ μž…λ‹ˆλ‹€. BalancerGroup close ν›„μ—λŠ” 더 이상 ν•˜μœ„ balancer μ—κ²Œ ExitIdle 호좜이 μ „νŒŒλ˜μ§€ μ•Šλ„λ‘ κ²€μ¦ν•©λ‹ˆλ‹€.

ExitIdle 의 κ°œλ…μ„ νŒŒμ•…ν•˜λ©΄ 사싀 λ„ˆλ¬΄λ‚˜ λ‹Ήμ—°ν•œ 검증 μ½”λ“œμž…λ‹ˆλ‹€. ExitIdle은 유휴 μƒνƒœμ˜ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό ν™œμ„±ν™”ν•˜μ—¬ μ„œλ²„μ™€μ˜ 연결을 μž¬κ°œν•˜λŠ” 것이 핡심 λ©”μ»€λ‹ˆμ¦˜μœΌλ‘œ, μΌλ°˜μ μœΌλ‘œλŠ” μƒˆλ‘œμš΄ RPC μš”μ²­μ΄ λ“€μ–΄μ˜€κ±°λ‚˜ λͺ…μ‹œμ μœΌλ‘œ Connect() λ©”μ„œλ“œκ°€ 호좜될 λ•Œ νŠΈλ¦¬κ±°λ˜μ–΄ IDLE μƒνƒœμ˜ SubConn 듀이 연결을 μ‹œλ„ν•©λ‹ˆλ‹€.

func (s) TestBalancerGroup_ExitIdle_AfterClose(t *testing.T) {
	balancerName := t.Name()
	exitIdleCh := make(chan struct{}, 1)

	stub.Register(balancerName, stub.BalancerFuncs{
		ExitIdle: func(_ *stub.BalancerData) {
			exitIdleCh <- struct{}{}
		},
	})

	bg := New(Options{
		CC:              testutils.NewBalancerClientConn(t),
		BuildOpts:       balancer.BuildOptions{},
		StateAggregator: nil,
		Logger:          nil,
	})

	bg.Add(testBalancerIDs[0], balancer.Get(balancerName))
	bg.Close()
	bg.ExitIdle()

	select {
	case <-exitIdleCh:
		t.Fatalf("ExitIdle was called on sub-balancer even after BalancerGroup was closed")
	case <-time.After(defaultTestShortTimeout):
	}
}

ExitIdle을 ν˜ΈμΆœν•˜λ©΄ exitIdleCh 채널에 μ‹ ν˜Έκ°€ μ˜€λ„λ‘ stub을 λ“±λ‘ν–ˆκ³ , select 문을 μ‚¬μš©ν•˜μ—¬ ν•΄λ‹Ή 채널에 μ‹ ν˜Έκ°€ μ˜€λŠ”μ§€ ν˜Ήμ€ defaultTestShortTimeout이 μ§€λ‚˜λ©΄ μ •μƒμ’…λ£Œ λ˜λ„λ‘ 두가지 μΌ€μ΄μŠ€λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€. μ’…λ£Œλœ BalancerGroupμ—μ„œ ExitIdle이 ν•˜μœ„ balancer에 μ „νŒŒλ˜μ§€ μ•ŠλŠ” 것이 정상 λ™μž‘μ΄κΈ° λ•Œλ¬Έμ— exitIdleChκ°€ μ‹ ν˜Έλ₯Ό λ°›μœΌλ©΄ ν…ŒμŠ€νŠΈλŠ” μ‹€νŒ¨ν•©λ‹ˆλ‹€.

μ‹€μ œλ‘œ ExitIdle κ΅¬ν˜„μ„ 보면 balancer group이 λ‹«νžŒ ν›„μ—” early return ν›„ graceful ν•˜κ²Œ μ²˜λ¦¬λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

// ExitIdle starts the balancing picker for use and exits idle mode.
func (bg *BalancerGroup) ExitIdle() {
    bg.outgoingMu.Lock()
    defer bg.outgoingMu.Unlock()
    if bg.outgoingClosed {
        return
    }
    for _, config := range bg.idToBalancerConfig {
        config.exitIdle()
    }
}

μœ„ 3개의 ν…ŒμŠ€νŠΈμ½”λ“œμ™€ μœ μ‚¬ν•œ λ©”μ»€λ‹ˆμ¦˜μœΌλ‘œ λ‚˜λ¨Έμ§€ 상황에 λŒ€ν•΄μ„œλ„ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν–ˆκ³  PR 을 마무리 ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

3. 마무리

grpc-go λ‚΄λΆ€ λ©”μ»€λ‹ˆμ¦˜ 쀑 BalancerGroup κ³Ό ExitIdle 의 λ™μž‘μ›λ¦¬λ₯Ό μ΄ν•΄ν•˜κ³  λ‹€μ–‘ν•œ μ‹œλ‚˜λ¦¬μ˜€μ— λŒ€ν•œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆλŠ” PR μ΄μ—ˆμŠ΅λ‹ˆλ‹€. 리뷰λ₯Ό λ°›κΈ° μ „ channel λŒ€μ‹  variable 을 μ‚¬μš©ν•΄ 검증 μ½”λ“œλ₯Ό μž‘μ„±ν–ˆμ„ 땐 λ‹€μŒκ³Ό 같은 리뷰λ₯Ό λ°›κ²Œ λμŠ΅λ‹ˆλ‹€. img.png κ°„λ‹¨νžˆ λ§ν•˜λ©΄ λ³€μˆ˜ λŒ€μ‹  채널을 μ‚¬μš©ν•˜λž€ 말인데, grpc 처럼 λ‚΄λΆ€μ μœΌλ‘œ λ§Žμ€ 고루틴을 μ‚¬μš©ν•˜λŠ” 라이브러리λ₯Ό ν…ŒμŠ€νŠΈν•  땐 race condition 에 λŒ€ν•œ 고렀도 ν•„μš”ν•˜κΈ° λ•Œλ¬Έμ— λ‚΄λΆ€μ μœΌλ‘œ 동기화가 보μž₯λ˜λŠ” channel 을 μ‚¬μš©ν•΄μ•Ό ν•œλ‹€λŠ” 것도 μ•Œ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.